<?xml version="1.0"?>
<!--
  Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/
-->
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>

<window title="DOMRequestHelper Test"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
        onload="start();">

  <script type="application/javascript"
          src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>

  <script type="application/javascript">
  <![CDATA[
    const Cc = Components.classes;
    const Cu = Components.utils;
    const Ci = Components.interfaces;
    Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
    let obs = Cc["@mozilla.org/observer-service;1"].
              getService(Ci.nsIObserverService);
    let ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"].
              getService(Ci.nsIMessageBroadcaster);

    function DummyHelperSubclass() {
      this.onuninit = null;
    }
    DummyHelperSubclass.prototype = {
      __proto__: DOMRequestIpcHelper.prototype,
      uninit: function() {
        if (typeof this.onuninit === "function") {
          this.onuninit();
        }
        this.onuninit = null;
      }
    };

    var dummy = new DummyHelperSubclass();
    var isDOMRequestHelperDestroyed = false;

    /**
     * Init & destroy.
     */
    function initDOMRequestHelperTest(aMessages) {
      // If we haven't initialized the DOMRequestHelper object, its private
      // properties will be undefined, but once destroyDOMRequestHelper is
      // called, they're set to null.
      var expectedPrivatePropertyValues =
        isDOMRequestHelperDestroyed ? null : undefined;

      is(dummy._requests, expectedPrivatePropertyValues, "Request is expected");
      is(dummy._messages, undefined, "Messages is undefined");
      is(dummy._window, expectedPrivatePropertyValues, "Window is expected");

      dummy.initDOMRequestHelper(window, aMessages);

      ok(dummy._window, "Window exists");
      is(dummy._window, window, "Correct window");
      if (aMessages) {
        is(typeof dummy._listeners, "object", "Listeners is an object");
      }
    }

    function destroyDOMRequestHelperTest() {
      dummy.destroyDOMRequestHelper();
      isDOMRequestHelperDestroyed = true;

      is(dummy._requests, null, "Request is null");
      is(dummy._messages, undefined, "Messages is undefined");
      is(dummy._window, null, "Window is null");
    }

    /**
     * Message listeners.
     */
    function checkMessageListeners(aExpectedListeners, aCount) {
      info("Checking message listeners\n" + "Expected listeners " +
           JSON.stringify(aExpectedListeners) + " \nExpected count " + aCount);
      let count = 0;
      Object.keys(dummy._listeners).forEach(function(name) {
        count++;
        is(aExpectedListeners[name].weakRef, dummy._listeners[name].weakRef,
           "Message found " + name + " - Same weakRef");
        is(aExpectedListeners[name].count, dummy._listeners[name].count,
           "Message found " + name + " - Same count");
      });
      is(aCount, count, "Correct number of listeners");
    }

    function addMessageListenersTest(aMessages, aExpectedListeners, aCount) {
      dummy.addMessageListeners(aMessages);
      info(JSON.stringify(dummy._listeners));
      checkMessageListeners(aExpectedListeners, aCount);
    }

    function removeMessageListenersTest(aMessages, aExpectedListeners, aCount) {
      dummy.removeMessageListeners(aMessages);
      checkMessageListeners(aExpectedListeners, aCount);
    }

    /**
     * Utility function to test window destruction behavior.  In general this
     * function does the following:
     *
     *  1) Create a new iframe
     *  2) Create a new DOMRequestHelper
     *  3) initDOMRequestHelper(), optionally with weak or strong listeners
     *  4) Optionally force a garbage collection to reap weak references
     *  5) Destroy the iframe triggering an inner-window-destroyed event
     *  6) Callback with a boolean indicating if DOMRequestHelper.uninit() was
     *     called.
     *
     * Example usage:
     *
     *    checkWindowDestruction({ messages: ["foo"], gc: true },
     *                           function(uninitCalled) {
     *      // expect uninitCalled === false since GC with only weak refs
     *    });
     */
    const TOPIC = "inner-window-destroyed";
    function checkWindowDestruction(aOptions, aCallback) {
      aOptions = aOptions || {};
      aOptions.messages = aOptions.messages || [];
      aOptions.gc = !!aOptions.gc;

      if (typeof aCallback !== "function") {
        aCallback = function() { };
      }

      let uninitCalled = false;

      // Use a secondary observer so we know when to expect the uninit().  We
      // can then reasonably expect uninitCalled to be set properly on the
      // next tick.
      let observer = {
        observe: function(aSubject, aTopic, aData) {
          if (aTopic !== TOPIC) {
            return;
          }
          obs.removeObserver(observer, TOPIC);
          setTimeout(function() {
            aCallback(uninitCalled);
          });
        }
      };

      let frame = document.createElement("iframe");
      frame.onload = function() {
        obs.addObserver(observer, TOPIC, false);
        // Create dummy DOMRequestHelper specific to checkWindowDestruction()
        let cwdDummy = new DummyHelperSubclass();
        cwdDummy.onuninit = function() {
          uninitCalled = true;

          if (!aOptions.messages || !aOptions.messages.length) {
            return;
          }

          // If all message listeners are removed, cwdDummy.receiveMessage
          // should never be called.
          ppmm.broadcastAsyncMessage(aOptions.messages[0].name);
        };

        // Test if we still receive messages from ppmm.
        cwdDummy.receiveMessage = function(aMessage) {
          ok(false, "cwdDummy.receiveMessage should NOT be called: " + aMessage.name);
        };

        cwdDummy.initDOMRequestHelper(frame.contentWindow, aOptions.messages);
        // Make sure to clear our strong ref here so that we can test our
        // weak reference listeners and observer.
        cwdDummy = null;
        if (aOptions.gc) {
          Cu.schedulePreciseGC(function() {
            SpecialPowers.DOMWindowUtils.cycleCollect();
            SpecialPowers.DOMWindowUtils.garbageCollect();
            SpecialPowers.DOMWindowUtils.garbageCollect();
            document.documentElement.removeChild(frame);
          });
          return;
        }
        document.documentElement.removeChild(frame);
      };
      document.documentElement.appendChild(frame);
    }

    /**
     * Test steps.
     */
    var tests = [
      function() {
        info("== InitDOMRequestHelper no messages");
        initDOMRequestHelperTest(null);
        next();
      },
      function() {
        info("== DestroyDOMRequestHelper");
        destroyDOMRequestHelperTest();
        next();
      },
      function() {
        info("== InitDOMRequestHelper empty array");
        initDOMRequestHelperTest([]);
        checkMessageListeners({}, 0);
        next();
      },
      function() {
        info("== DestroyDOMRequestHelper");
        destroyDOMRequestHelperTest();
        next();
      },
      function() {
        info("== InitDOMRequestHelper with strings array");
        initDOMRequestHelperTest(["name1", "nameN"]);
        checkMessageListeners({"name1": {weakRef: false, count: 1},
                               "nameN": {weakRef: false, count: 1}}, 2);
        next();
      },
      function() {
        info("== DestroyDOMRequestHelper");
        destroyDOMRequestHelperTest();
        next();
      },
      function() {
        info("== InitDOMRequestHelper with objects array");
        initDOMRequestHelperTest([{
          name: "name1",
          weakRef: false
        }, {
          name: "nameN",
          weakRef: true
        }]);
        checkMessageListeners({
          "name1": {weakRef: false, count: 1},
          "nameN": {weakRef: true,  count: 1}
        }, 2);
        next();
      },
      function() {
        info("== AddMessageListeners empty array");
        addMessageListenersTest([], {
        "name1": {weakRef: false, count: 1},
        "nameN": {weakRef: true,  count: 1}
        }, 2);
        next();
      },
      function() {
        info("== AddMessageListeners null");
        addMessageListenersTest(null, {
          "name1": {weakRef: false, count: 1},
          "nameN": {weakRef: true,  count: 1}
          }, 2);
        next();
      },
      function() {
        info("== AddMessageListeners new listener, string only");
        addMessageListenersTest("name2", {
          "name1": {weakRef: false, count: 1},
          "name2": {weakRef: false, count: 1},
          "nameN": {weakRef: true,  count: 1}
        }, 3);
        next();
      },
      function() {
        info("== AddMessageListeners new listeners, strings array");
        addMessageListenersTest(["name3", "name4"], {
          "name1": {weakRef: false, count: 1},
          "name2": {weakRef: false, count: 1},
          "name3": {weakRef: false, count: 1},
          "name4": {weakRef: false, count: 1},
          "nameN": {weakRef: true,  count: 1}
        }, 5);
        next();
      },
      function() {
        info("== AddMessageListeners new listeners, objects array");
        addMessageListenersTest([{
          name: "name5",
          weakRef: true
        }, {
          name: "name6",
          weakRef: false
        }], {
          "name1": {weakRef: false, count: 1},
          "name2": {weakRef: false, count: 1},
          "name3": {weakRef: false, count: 1},
          "name4": {weakRef: false, count: 1},
          "name5": {weakRef: true,  count: 1},
          "name6": {weakRef: false, count: 1},
          "nameN": {weakRef: true,  count: 1}
        }, 7);
        next();
      },
      function() {
        info("== RemoveMessageListeners, null");
        removeMessageListenersTest(null, {
          "name1": {weakRef: false, count: 1},
          "name2": {weakRef: false, count: 1},
          "name3": {weakRef: false, count: 1},
          "name4": {weakRef: false, count: 1},
          "name5": {weakRef: true,  count: 1},
          "name6": {weakRef: false, count: 1},
          "nameN": {weakRef: true,  count: 1}
        }, 7);
        next();
      },
      function() {
        info("== RemoveMessageListeners, one message");
        removeMessageListenersTest("name1", {
          "name2": {weakRef: false, count: 1},
          "name3": {weakRef: false, count: 1},
          "name4": {weakRef: false, count: 1},
          "name5": {weakRef: true,  count: 1},
          "name6": {weakRef: false, count: 1},
          "nameN": {weakRef: true,  count: 1}
        }, 6);
        next();
      },
      function() {
        info("== RemoveMessageListeners, array of messages");
        removeMessageListenersTest(["name2", "name3"], {
          "name4": {weakRef: false, count: 1},
          "name5": {weakRef: true,  count: 1},
          "name6": {weakRef: false, count: 1},
          "nameN": {weakRef: true,  count: 1}
        }, 4);
        next();
      },
      function() {
        info("== RemoveMessageListeners, unknown message");
        removeMessageListenersTest("unknown", {
          "name4": {weakRef: false, count: 1},
          "name5": {weakRef: true,  count: 1},
          "name6": {weakRef: false, count: 1},
          "nameN": {weakRef: true,  count: 1}
        }, 4);
        next();
      },
      function() {
        try {
          info("== AddMessageListeners, same message, same kind");
          addMessageListenersTest("name4", {
            "name4": {weakRef: false, count: 2},
            "name5": {weakRef: true,  count: 1},
            "name6": {weakRef: false, count: 1},
            "nameN": {weakRef: true,  count: 1}
          }, 4);
          next();
        } catch (ex) {
          ok(false, "Unexpected exception " + ex);
        }
      },
      function() {
        info("== AddMessageListeners, same message, different kind");
        try {
          addMessageListenersTest({name: "name4", weakRef: true}, {
            "name4": {weakRef: false, count: 2},
            "name5": {weakRef: true,  count: 1},
            "name6": {weakRef: false, count: 1},
            "nameN": {weakRef: true,  count: 1}
          }, 4);
          ok(false, "Should have thrown an exception");
        } catch (ex) {
          ok(true, "Expected exception");
          next();
        }
      },
      function() {
        info("== RemoveMessageListeners, message with two listeners");
        try {
          removeMessageListenersTest(["name4", "name5"], {
            "name4": {weakRef: false, count: 1},
            "name6": {weakRef: false, count: 1},
            "nameN": {weakRef: true,  count: 1}
          }, 3);
          next();
        } catch (ex) {
          ok(false, "Unexpected exception " + ex);
        }
      },
      function() {
        info("== Test createRequest()");
        ok(DOMRequest, "DOMRequest object exists");
        var req = dummy.createRequest();
        ok(req instanceof DOMRequest, "Returned a DOMRequest");
        next();
      },
      function() {
        info("== Test getRequestId(), removeRequest() and getRequest()");
        var req = dummy.createRequest();
        var id = dummy.getRequestId(req);
        is(typeof id, "string", "id is a string");
        var req_ = dummy.getRequest(id);
        is(req, req_, "Got correct request");
        dummy.removeRequest(id);
        req = dummy.getRequest(id);
        is(req, undefined, "No request");
        next();
      },
      function() {
        info("== Test createPromise()");
        ok(Promise, "Promise object exists");
        var promise = dummy.createPromise(function(resolve, reject) {
          resolve(true);
        });
        ok(promise instanceof Promise, "Returned a Promise");
        promise.then(next);
      },
      function() {
        info("== Test createPromiseWithId()");
        var _resolverId;
        var promise = dummy.createPromiseWithId(function(resolverId) {
          _resolverId = resolverId;
        });
        var resolver = dummy.getPromiseResolver(_resolverId);
        ok(promise instanceof Promise, "Returned a Promise");
        ok(typeof _resolverId === "string", "resolverId is a string");
        ok(resolver != null, "resolverId is a valid id");
        next();
      },
      function() {
        info("== Test getResolver()");
        var id;
        var resolver;
        var promise = dummy.createPromise(function(resolve, reject) {
          var r = { resolve: resolve, reject: reject };
          id = dummy.getPromiseResolverId(r);
          resolver = r;
          ok(typeof id === "string", "id is a string");
          r.resolve(true);
        }).then(function(unused) {
          var r = dummy.getPromiseResolver(id);
          ok(resolver === r, "Get succeeded");
          next();
        });
      },
      function() {
        info("== Test removeResolver");
        var id;
        var promise = dummy.createPromise(function(resolve, reject) {
          var r = { resolve: resolve, reject: reject };
          id = dummy.getPromiseResolverId(r);
          ok(typeof id === "string", "id is a string");

          var resolver = dummy.getPromiseResolver(id);
          info("Got resolver " + JSON.stringify(resolver));
          ok(resolver === r, "Resolver get succeeded");

          r.resolve(true);
        }).then(function(unused) {
          dummy.removePromiseResolver(id);
          var resolver = dummy.getPromiseResolver(id);
          ok(resolver === undefined, "removeResolver: get failed");
          next();
        });
      },
      function() {
        info("== Test takeResolver");
        var id;
        var resolver;
        var promise = dummy.createPromise(function(resolve, reject) {
          var r = { resolve: resolve, reject: reject };
          id = dummy.getPromiseResolverId(r);
          resolver = r;
          ok(typeof id === "string", "id is a string");

          var gotR = dummy.getPromiseResolver(id);
          ok(gotR === r, "resolver get succeeded");

          r.resolve(true);
        }).then(function(unused) {
          var r = dummy.takePromiseResolver(id);
          ok(resolver === r, "take should succeed");

          r = dummy.getPromiseResolver(id);
          ok(r === undefined, "takeResolver: get failed");
          next();
        });
      },
      function() {
        info("== Test window destroyed without messages and without GC");
        checkWindowDestruction({ gc: false }, function(uninitCalled) {
          ok(uninitCalled, "uninit() should have been called");
          next();
        });
      },
      function() {
        info("== Test window destroyed without messages and with GC");
        checkWindowDestruction({ gc: true }, function(uninitCalled) {
          ok(!uninitCalled, "uninit() should NOT have been called");
          next();
        });
      },
      function() {
        info("== Test window destroyed with weak messages and without GC");
        checkWindowDestruction({ messages: [{ name: "foo", weakRef: true }],
                                 gc: false }, function(uninitCalled) {
          ok(uninitCalled, "uninit() should have been called");
          next();
        });
      },
      function() {
        info("== Test window destroyed with weak messages and with GC");
        checkWindowDestruction({ messages: [{ name: "foo", weakRef: true }],
                                 gc: true }, function(uninitCalled) {
          ok(!uninitCalled, "uninit() should NOT have been called");
          next();
        });
      },
      function() {
        info("== Test window destroyed with strong messages and without GC");
        checkWindowDestruction({ messages: [{ name: "foo", weakRef: false }],
                                 gc: false }, function(uninitCalled) {
          ok(uninitCalled, "uninit() should have been called");
          next();
        });
      },
      function() {
        info("== Test window destroyed with strong messages and with GC");
        checkWindowDestruction({ messages: [{ name: "foo", weakRef: false }],
                                 gc: true }, function(uninitCalled) {
          ok(uninitCalled, "uninit() should have been called");
          next();
        });
      }
    ];

    function next() {
      if (!tests.length) {
        SimpleTest.finish();
        return;
      }

      var test = tests.shift();
      test();
    }

    function start() {
      SimpleTest.waitForExplicitFinish();
      next();
    }
  ]]>
  </script>

  <body xmlns="http://www.w3.org/1999/xhtml">
    <p id="display"></p>
    <div id="content" style="display: none"></div>
    <pre id="test"></pre>
  </body>
</window>
